#include "SortedBufferManager.h"

/**
Create a new instance that manages size frames and operates on the file filename.
*/
SortedBufferManager::SortedBufferManager(const std::string& filename, unsigned size) : BufferManager(filename, size), flushPages(10)
{
    if (size <= 30)
    {
        flushPages = 0.15 * size;
        flushPages = (flushPages == 0)?1:flushPages;
    }

    SEM_CREATE(this->freeQueueSemaphore);

    for (unsigned i = 0; i < size; ++i)
    {
        this->freeQueue.push(i);
    }
}

/**
A method to retrieve frames given a page ID and indicating whether the page will be
held exclusively by this thread or not. The method can fail if no free frame is
available and no used frame can be freed.
*/
BufferFrame& SortedBufferManager::getPage(unsigned pageId, bool exclusive)
{
    int position = this->getIndex(pageId);

    if (position == -1)
    {
        //frame not found

        // find free cell or swap page if there's no free space
        SEM_WRITELOCK(this->freeQueueSemaphore);
        if (this->freeQueue.size()>0)
        {
            position = this->freeQueue.front();
            this->freeQueue.pop();
        }
        else
        {
            position = this->swapPage();
        }
        SEM_UNLOCK(this->freeQueueSemaphore);

        // increment MISSES counter
        this->bufferMisses += 1;

        if (position == -1)
        {
            //no free frame...now we're doomed
            cout << "Something bad happened: Sry dude, no buffer-space left." << endl;
            exit(1);
        }

        this->frames[position] = this->loadPage(pageId);
        BufferFrame& frame = *(this->frames[position]);
        frame.doLock(exclusive);

        SEM_WRITELOCK(this->indexSemaphore);
        this->index[pageId] = position;
        SEM_UNLOCK(this->indexSemaphore);

        SEM_WRITELOCK(this->frameQueueSemaphore);
        this->frameQueue.push(position);
        SEM_UNLOCK(this->frameQueueSemaphore);

        //  SEM_UNLOCK(this->arrayLocks[position]);

        return frame;
    }

    // increment HIT counter
    SEM_WRITELOCK(this->hitSemaphore);
    this->bufferHits += 1;
    SEM_UNLOCK(this->hitSemaphore);

    SEM_READLOCK(this->arrayLocks[position]);

    BufferFrame& frame = *(this->frames[position]);

    if (frame.getPageId() != pageId)
    {
        SEM_UNLOCK(this->arrayLocks[position]);
        return this->getPage(pageId, exclusive);
    }

    frame.doLock(exclusive);
    SEM_UNLOCK(this->arrayLocks[position]);

    return frame;
}

/**
Swaps a unused page to disc and returns the index of the freed page.
Return -1 if no frame is free. If x is returned, this->arrayLocks[x] is locked.
*/
int SortedBufferManager::swapPage()
{
    SEM_WRITELOCK(this->frameQueueSemaphore);

    if (this->frameQueue.size() == 0)
    {
        SEM_UNLOCK(this->frameQueueSemaphore);
        return -1; //failure
    }

    list<BufferFrame*> pagesToFlush;
    int result = -1;

    //SEM_WRITELOCK(this->freeQueueSemaphore);

    // visit each page twice for second chance
    for (unsigned i = 0; i < 2 * this->frameQueue.size(); i++)
    {
        unsigned position = this->frameQueue.front();
        this->frameQueue.pop();

        if (SEM_TRYWRITELOCK(this->arrayLocks[position]) != 0)
        {
            //lock failed -> frame is in use
            this->frameQueue.push(position);
        }
        else
        {
            //lock successfull
            BufferFrame& frame = *(this->frames[position]);

            if (frame.tryLock() != 0)
            {
                //lock failed -> frame is in use
                this->frameQueue.push(position);
                SEM_UNLOCK(this->arrayLocks[position]);
            }
            else
            {
                //lock successfull
                if (frame.hasSecondChance())
                {
                    frame.setSecondChance(false);
                    this->frameQueue.push(position);
                    frame.releaseLock();
                    SEM_UNLOCK(this->arrayLocks[position]);
                }
                else
                {
                    //SEM_UNLOCK(this->frameQueueSemaphore);
                    SEM_WRITELOCK(this->indexSemaphore);
                    this->index[this->frames[position]->getPageId()] = -1;
                    SEM_UNLOCK(this->indexSemaphore);
                    SEM_UNLOCK(this->arrayLocks[position]);

                    this->freeQueue.push(position);
                    result = position;
                    pagesToFlush.insert(pagesToFlush.end(), this->frames[position]);
                    if (pagesToFlush.size() >= flushPages) break;
                }
            }
        }
    }
    SEM_UNLOCK(this->frameQueueSemaphore);

    this->sortPages(pagesToFlush);

    this->writeToDisk(pagesToFlush);

    //SEM_UNLOCK(this->freeQueueSemaphore);
    if (result != -1)
    {
        result = this->freeQueue.front();
        this->freeQueue.pop();
    }
    return result;
}

void SortedBufferManager::sortPages(list<BufferFrame*>& pageList)
{
    pageList.sort();
}

/**
Writes the data of the given frames back to disk if the frames are marked as dirty. All frames are being deleted. Alter execution of this method the list will be empty.
*/
void SortedBufferManager::writeToDisk(list<BufferFrame*>& frames)
{
    SEM_WRITELOCK(this->fileSystemLock);

    while (frames.size()>0)
    {
        BufferFrame& frame = *(frames.front());
        frames.pop_front();

        if (frame.isDirty())
        {
            if (fseek(this->file, frame.getPageId() * pageSize, SEEK_SET) != 0)
            {
                cout << "Something bad happened: error while seeking file." << endl;
                exit(1);
            }
            if (fwrite(frame.getData(), sizeof(char), pageSize, this->file) != pageSize)
            {
                cout << "Something bad happened: error while writing file." << endl;
                exit(1);
            }
        }
        delete &frame;
    }

    SEM_UNLOCK(this->fileSystemLock);
}


SortedBufferManager::~SortedBufferManager()
{
    SEM_DESTROY(this->freeQueueSemaphore);

    while (this->freeQueue.size() > 0)
    {
        this->frames[this->freeQueue.front()] = NULL;
        this->freeQueue.pop();
    }
}
